/*! 
 * jquery.ux.sortable - v 1.0.0 - http://jquery-ux.com/
 * Copyright (c) 2010 Michael Helgeson, Three Dub Media
 * Open Source MIT License - http://jquery-ux.com/license 
 */
// Created: 2007-02-14
// Updated: 2010-01-22
// REQUIRES: jquery 1.4+, jquery.ux

;(function( $ ){ // confine scope

$.ux( "sortable",{ // Register the PLUGIN... and public methods...
	expose: "reset sort page", // + enable disable destroy
	defaults:{
		sort: 0, // {integer} initial sort column
		desc: false, // {boolean} initial sort direction
		page: 0, // {integer} initial page to display
		perpage: null, // {integer|null} rows to show at one time
		resetPage: false, // {boolean} go to page 1 on each sort
		stripes: null, // {string|array|null} zebra striping 
		cache: true, // {boolean} cache the sort data or not		
		columns: [], // {array} column data type parsers
		head: null, // {string} where to bind sortable clicks
		delegate: "td", // the query selector match to allow group toggling
		paginate: function( $span ){ // {function|null} callback to append pagination
			$span.insertAfter( this ).wrap('<p></p>');
			},
		classroot: "ux-sortable"
		// table: "ux-sortable",
		// ascending: "ux-sortable-asc",
		// descending: "ux-sortable-dsc",
		// group: "ux-sortable-group",
		// closed: "ux-sortable-closed"
		},
	prepare: function( opts ){
		if ( !opts ) return;
		// make array from space seperated values
		if ( typeof opts.stripes == "string" )
			opts.stripes = opts.stripes.split(" ");
		// normalize the column instructions
		$.each( opts.columns || [], function( i, val ){
			var parse = val.parse || val;
			opts.columns[ i ] = val === false ? false : {
				// return false to exclude a row
				parse: $.sortable.parse[ parse ] ? $.sortable.parse[ parse ] : 
					parse && $.isFunction( parse ) ? parse : ( parse || false ),
				// determine a group id	
				group: val && val.group || false,
				// return a group <row> which toggles open/closed
				make: val && val.make || false,
				// the group contents initial state 
				closed: val && val.closed || false
				};						   
			});
		// return modified options
		return opts;
		},
	create: function( elem, opts ){
		this.$elem = $( elem ).addClass( this.classify("") );
		// configure by nodename
		switch ( elem.nodeName.toLowerCase() ){
			case "ul": case "ol":
				this.$tbody = this.$elem;
				this.tr = "li";
				break;
			case "table":
				this.$head = this.$elem.find('thead td');
				this.$tbody = this.$elem.find("tbody");
				this.tr = "tr";
				this.td = "td";
				break;
			case "select":
				this.$tbody = $( elem );
				this.tr = "option";
				break;
			default: 
				return false;
			}
		// store args, extend the instance
		$.extend( this, { elem:elem, opts:opts });
		// bind click to the table headers
		this.$head = $( opts.head || this.$head || "<div/>" )
			.bind( "click", $.proxy( this, "handler") );
		// bind click to the table bodies
		this.$tbody.bind( "click", $.proxy( this, "handler") );
		// initialize row cache, perpage, sorting and row stripes	
		this.reset();
		this.sort( parseInt( opts.sort ) || 0, !!opts.desc );
		this.page( parseInt( opts.page ) || 0 );
		},
	destroy: function(){
		this.sort( -1 ); // unsort/ungroup
		this.$head.unbind( "click", $.proxy( this, "handler") );
		this.$tbody.unbind( "click", $.proxy( this, "handler") );
		this.unpaginate();
		},
	methods: {
		// handle events
		handler: function( event ){
			var column = this.$head.index( event.currentTarget ), 
			rows, $elem = $( event.target );
			// "click" in a header/controller
			if ( column > -1 ) return !!this.sort( column ); 
			// pagination "clicks"
			if ( this.$pagination && $elem.closest("span")[0] == this.$pagination[0] ){
				this.page( $elem.attr("href").split('#')[1] );
				return false;
				}
			// check the delegate options
			if ( !$elem.closest( this.opts.delegate ).length ) return;	
			// look for groups
			$elem = $elem.closest("."+ this.classify("group") );
			rows = $elem.data("sortable:group");
			// "click" on a group row
			if ( rows ){
				$elem.toggleClass( this.classify("closed") );
				$( rows )[ $elem.is("."+ this.classify("closed") ) ? "hide" : "show" ]();
				this.stripe();
				return false;
				}				
			},
		// clear all cached, start anew 
		reset: function(){
			this.rows = [];
			this.cache = [];
			this.groups = [];
			var self = this;
			// lookup and cache the sortable rows
			this.$tbody.each(function( tb ){ 
				self.rows[ tb ] = $( this ).find( self.tr ); 
				self.cache[ tb ] = [];
				self.groups[ tb ] = {};
				}); 
			// pagination is only supported for one tbody
			this.perpage = parseInt( this.opts.perpage ) || false;	
			if ( this.perpage ){
				// calculate the total number of pages...
				this.total = this.rows[0].length;
				this.pages = Math.ceil( this.total / this.perpage );
				// execute pagination callback
				this.unpaginate();
				if ( this.pages > 1 ) this.paginate();
				}
			},
		// create the pagination...
		paginate: function(){
			var self = this, i, start, end,
			// build up the pagination controls
			txt = '<span><a href="#prev">&laquo; prev</a> <select>';
			// iterate each page...
			for ( i = 0; i < this.pages; i++ )
				txt += '<option value="'+ i +'">'
					+ ( i * this.perpage + 1 ) +' - '
					+ Math.min( ( i + 1 )*this.perpage, this.total ) 
					+' of '+ this.total +'</option>';
			txt += '</select> <a href="#next">next &raquo;</a></span>';
			// insert and bind events to generated markup
			this.$pagination = $( txt ).find('a')
				.bind( "click", $.proxy( this, "handler") ).end();
			this.$select = this.$pagination
				.find("select").bind("change",function(){
					self.page( this.selectedIndex );								   
					});
			// insert the pagination
			if ( this.opts.paginate )
				this.opts.paginate.call( this.elem, this.$pagination );
			},
		// remove he pagination
		unpaginate: function(){
			if ( this.$pagination )
				this.$pagination.remove();
			},
		// apply row zebra striping
		stripe: function(){
			var stripes = this.opts.stripes, string;
			if ( stripes && stripes.length ){
				string = stripes.join(" ");
				this.$tbody.find( this.tr ).each(function( tr ){								
					if ( $( this ).is(":visible") ) 
						$( this ).removeClass( string )
							.addClass( stripes[ tr % stripes.length ] );	
					});
				}
			return this;
			},
		// publish a custom event, and return boolean
		callback: function(){ 
			return !this.publish.apply( this, arguments ).isDefaultPrevented(); 
			},
		// read or lookup cached data for sorting
		findData: function( bod, desc ){
			// some local refs for closures
			var self = this, opts = this.opts, col = this.active, $cell,
			// parsing and grouping options for active column
			parse = ( opts.columns[ col ] || {} ).parse, text, value, values, 
			group = ( opts.columns[ col ] || {} ).group, groupname,
			// read a copy from the cache...
			data = ( this.cache[ bod ][ col ] || [] ).slice();			
			// if the cache is empty
			if ( !data.length ){
				// read and parse the rows
				this.rows[ bod ].each(function( row ){
					$cell = ( self.td ? $( this ).find( self.td ) : $( this ) ).eq( col );
					text = $.trim( $cell.text() );
					// the parse function... 
					value = parse ? parse.call( $cell[0], text ) : text;
					// exclude/filter: return false
					if ( value === false ) return;
					// multi/custom sort: return an array of values
					if ( ( value ).constructor == Array ) values = value
					// normal sort: return a single parsed value
					else values = [ value ];
					// add the cached index to the values as fallback
					values.push( row );
					// find the group name
					groupname = group ? group.call( $cell[0], values, text ) : false;
					// add the groupname to the front if only 2 values
					if ( groupname && values.length == 2 ) values.unshift( groupname );
					// add the data to the cache...
					data.push({ 
						index: row, 
						text: text,
						value: value, 
						group: groupname,
						values: values
						});	
					});
				// sort the data ascending by group, parsed value, original order
				data.sort(function( a, b ){ 
					for ( var i = 0, x; i < a.values.length && !x; i++ )
						x = a.values[i] < b.values[i] ? -1 : a.values[i] > b.values[i] ? 1 : 0;
					return x; 
					});
				// cache the data (or not)...
				if ( opts.cache ) this.cache[ bod ][ col ] = data; 
				}
			// return the ordered and trimmed data...
			return ( desc ? data.reverse() : data ).slice( this.start, this.end || data.length );	
			},
		// execute the sorting by column index	
		sort: function( col, desc ){ 
			// validate the column index argument
			if ( this.opts.columns[ col ]===false ) return false;
			// disabled and "sortable:before:sort" callback
			if ( this.disabled || !this.callback("before:sort") ) return; 
			// set the active column...
			col = this.active = !isNaN( col ) && col < this.$head.length ? col : this.active;
			// ref some classnames
			var asc = this.classify("asc"), dsc = this.classify("dsc");
			// detemine the sort direction...
			this.desc = desc = desc == undefined ? this.$head.eq( col ).is("."+ asc ) : !!desc;
 			// remove any existing sort classes, set the actual sort class
			this.$head.removeClass( asc +" "+ dsc ).eq( this.active ).addClass( desc ? dsc : asc ); 
			// determine if the page must be reset... do the heavy lifting
			( this.opts.resetPage && this.current ) ? this.page( 0 ) : this.render();
			// callback
			this.callback("sort"); // "sortable:sort"
			},
		// renders the actual rows in order and visibility
		render: function(){
			// ref some variables for closure
			var self = this, col = this.active, 
			column = this.opts.columns[ col ] || {}, 
			$row, $body, $group, group, groupData, closed;
			// iterate one or more list bodies
			this.$tbody.each(function( bod ){
				// hide every row for pagination or filter
				if ( self.perpage || column.filter ) 
					self.rows[ bod ].hide();
				// remove any existing groupings
				$.each( self.groups[ bod ] || {}, function(){
					// if the group is closed...
					if ( this.$group.is("."+ self.classify("closed") ) ){
						this.$group.removeClass( self.classify("closed") );
						$( this.rows ).show();
						}
					// remove the row, if allowed	
					if ( !this.keep ) this.$group.remove();								
					// otherwise, at least de-classify
					else this.$group.removeClass( self.classify("group") );
					});
				// the sort column exists
				if ( col > -1 ){		
					// find the data (already sorted)...
					$.each( self.findData( bod, self.desc ), function( row, data ){
						$body = self.$tbody.eq( bod );
						$row = $( self.rows[ bod ][ data.index ] );
						// do some grouping stuff...
						if ( data.group !== false ){
							// ref the group data...
							groupData = self.groups[ bod ][ col +":"+ data.group ];
							// determine if the group should be open or closed
							closed = !!column.closed;
							// this group is different from current
							if ( data.group != group && column.make ){
								// find the group row...
								if ( groupData ) $group = groupData.$group;
								// make the group row
								else $group = column.make.call( $row[0], data.group, data.value, data.text );
								// store the group data...
								groupData = self.groups[ bod ][ col +":"+ data.group ] = { $group: $group, rows: [] };								
								// insert the group row, add classes, store group data...
								if ( $group ) $group.addClass( self.classify("group") )
									.appendTo( $body ).show().data("sortable:group", groupData.rows );
								// set the appropriate group state (open/closed)
								$group[ closed ? "addClass" : "removeClass" ]( self.classify("closed") );
								// set the group name for compare
								group = data.group;
								}
							// the row matched the current group
							if ( groupData && data.group == group ){
								// if the row matches the group row, skip it
								if ( groupData.$group[0] == $row[0] ){
									// remember to not remove it later on
									groupData.keep = true;
									// skip appending
									return;
									}
								// just add to the group data rows cache, set group state (open/closed)
								groupData.rows.push( $row[ closed ? "hide" : "show" ]()[0] );
								}
							}
						// no group, make sure the row is showing	
						else $row.show();
						// move the row
						$body.append( $row ); // move the row
						});
					}
				// undo sorting
				else self.rows[ bod ].each(function( i, row ){ 
					self.$tbody.eq( bod ).append( row ); 
					}); 
				});
			this.stripe();
			},
		// sets the current page number, then re-sorts
		page: function( index ){
			// optional string arguments...
			var lookup = { 
				"first": 0,
				"prev": this.current - 1,
				"next": this.current + 1,
				"last": this.pages - 1
				};
			// convert argument into a real value
			index = parseInt( lookup[ index ] == undefined ? index : lookup[ index ] );
			// apply circular flow logic
			if ( index > lookup.last ) index = 0;
			if ( index < lookup.first ) index = lookup.last;
			// ignore bad arguments...
			if ( isNaN( index ) ) return;
			this.current = index;
			// set the indexes for slicing up data...
			this.start = this.perpage ? index * this.perpage : 0; 
			this.end = this.perpage ? this.start + this.perpage : undefined;
			// do the heavy lifting
			this.render();
			// set pagination
			if ( this.$select ) this.$select.val( index );
			this.callback("page", index );
			}
		}
	});

$.sortable.parse = {
	"abc": function( str ){ 
		return str.toLowerCase(); 
		},
	"123": function( str ){ 
		return parseFloat( str.replace(/\$|,/g,"") ); 
		},
	"date": function( str ){
		// mm/dd/yyyy
		var ymd = (/([[a-z0-9]+)[^a-z0-9]+(\d{1,2})[^a-z0-9]+(\d{4})/i).exec( str );
		if ( ymd ) return new Date( ymd.slice(1).join('/') ).getTime();
		// yyyy/mm/dd
		ymd = (/(\d{4})[^a-z0-9]+([a-z0-9]+)[^a-z0-9]+(\d{1,2})/i).exec( str );
		return new Date( ymd ? ymd[2] +'/'+ ymd[3] +'/'+ ymd[1] : str ).getTime();
		}
	};

})( jQuery ); // secure the $ jQuery alias